////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////@

// Δημιουργούμε το singelton "DB" το οποίο αφορά στην επαφή μας με την database.
// Περιέχει connection pool, connection free stack και σχετικές functions.

DB = {};

Log.level.push();
Log.print('reading "MySQL" module');
DB.mysql = require('/usr/local/lib/node_modules/mysql');
Log.level.pop();

// Ακολουθεί το connection pool. Πρόκειται για array συνδέσεων με την database.
// Κάθε νέα connection προστίθεται στο array. Εφόσον φροντίζουμε να απελευθερώνουμε
// τα database connections μετά την εκτέλεση των σχετικών queries, τα connections
// μπορούν να ξαναχρησιμοποιηθούν αργότερα. Το πρόγραμμα φροντίζει ώστε τα
// connections να παραμένουν ενεργά δίνοντας ψευδοqueries όταν παρέλθει αρκετός
// χρόνος κατά τον οποίο τα connections παραμένουν ανενεργά.

DB.pool = [];

// Ακουλουθεί το free stack του connection pool. Κάθε connection που δεν χρειάζεται
// πια, τοποθετείται στο free stack και το connection ανασύρεται πάλι όταν χρειαστούμε
// νέο. Στο free stack δεν κρατάμε τα ίδια τα connections αλλά τους αντίστοιχους δείκτες
// από το connection pool.

DB.freeStack = [];

// Η flag "debug" δείχνει αν τα queries θα καταγράφονται στο console log.

DB.debug = true;
DB.debug = false;

// Η function "connection" μας επιστρέφει ένα connection προς την database είτε από
// το connection pool, εφόσον υπάρχει ελεύθερο connection, είτε δημιουργεί νέο.

DB.connection = function() {
	var conn;

	if (!DB.freeStack.length) return new DBSindesi();

	conn = DB.pool[DB.freeStack.pop()];
	if (conn.active) globals.fatal('active database connection detected in the free stack');
	conn.active = true;
	return conn;
}

// Η function "reset" κλείνει όλα τα database connections και "μηδενίζει" το connection
// pool και το connection free stack.

DB.reset = function() {
	Log.print('closing database connections');
	globals.walk(DB.pool, function(i, conn) {
		conn.connection.end();
	});

	DB.pool = [];
	DB.freeStack = [];
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////@

// Η function "setTimeout" καλείται κατά την εκκίνηση τού skiser με σκοπό να ανιχνεύσει τις
// timeout τιμές του SQL server. Εκτελείται το παρακάτω query
//
//	SELECT @@interactive_timeout AS `ito`, @@wait_timeout AS `wto`
//
// το οποίο πρέπει να επιστρέφει μια γραμμή και αυτήν τη γραμμή εξετάζει η "setTimeout".
// Και οι δύο τιμές χρησιμοποιούνται από τον SQL server για να ελέγχει τυχόν "ξεχασμένα"
// connections. Εμείς θα χρησιμοποιήσουμε την μικρότερη από τις δύο τιμές για να προλάβουμε
// τον SQL server. Όσον αφορά σε "ξεχασμένα" connections θα λάβουμε τα μέτρα μας πριν φτάσει
// το πρόβλημα στον SQL server.

DB.setTimeout = function(rows) {
	var sqlTimeout;

	if (rows.length != 1) throw 'DB.setTimeout: failed to get sql timeout values';

	sqlTimeout = Math.floor(rows[0].ito < rows[0].wto ? rows[0].ito : rows[0].wto);
//sqlTimeout = 24;

	// Οι τιμές των παραμέτρων δίνονται σε seconds. Αφού τις μετατρέψουμε σε milliseconds,
	// φροντίζουμε να θέσουμε τις περιόδους ελέγχου σε τιμές που να διασφαλίζουν ότι οι
	// συνδέσεις με την database δεν θα λήξουν.

	Log.print('MySQL database connection timeout: ' + sqlTimeout + ' seconds');

	// Ορίζουμε τακτικό έλεγχο ανενεργών συνδέσεων περίπου στο μισό του μέγιστου
	// επιτρεπτού χρόνου ανενεργών συνδέσεων.

	peripolos.ergasia.dbconn.period = Math.floor(sqlTimeout / 2);
	Log.print('database connection patrol cycle: ' + peripolos.ergasia.dbconn.period + ' seconds');

	// Κατά τον έλεγχο θα επανεργοποιήσουμε συνδέσεις που είναι ανενεργές για περίπου
	// το 90% του χρόνου της περιόδου ελέγχου, επομένως προλαμβάνουμε την ανίχνευση
	// του προβλήματος από τον SQL server.

	DB.timeout = Math.floor(peripolos.ergasia.dbconn.period * 0.9);
	Log.print('database connection idle max: ' + DB.timeout + ' seconds');

	// Τα timeout values είναι αρκετά μεγάλα (τάξης ωρών) by default. Αν η παραπάνω
	// διαιδκασία κατέληξε σε τιμή μικρότερη των 10 seconds, τότε κάτι δεν πάει καλά.

	if (DB.timeout < 10) throw 'DB.setTimeout: too small database connection timeout (' + timeout + ' seconds)';

	// Οι τιμές μετατρέπονται σε milliseconds.

	peripolos.ergasia.dbconn.period *= 1000;
	DB.timeout *= 1000;
}

// Αν το connection δεν εκτελέσει κανένα πραγματικό query για πάνω από μισή ώρα, τότε
// θεωρείται zombie και επανατοποθετείται στο free stack. Η property "zombie" σκοπό έχει
// να θεραπεύσει τυχόν προγραμματιστικά σφάλματα μέσω των οποίων μπορεί να δημιουργούνται
// διαρροές συνδέσεων.

DB.zombie = 1800000;

// Η μέθοδος "check" καλείται στα πλαίσια τακτικού περιοδικού ελέγχου και σκοπό έχει την
// επανεργοποίηση ανενεργών συνδέσεων προκειμένου αυτές να μην κλείσουν από τον database
// server. Ο έλεγχος αφορά σε όλες τις συνδέσεις και όχι μόνο σ' αυτές που έχουν κλείσει
// τον κύκλο τους και έχουν τοποθετηθεί στο free stack. Βέβαια, οι συνδέσεις που είναι
// ενεργές και εκτελούν κάποια queries είναι μάλλον απίθανο να επανεργοποιηθούν, καθώς
// δεν θα υπερβαίνουν το χρονικό όριο απενεργοποίησης. Όλες οι χρονικές τιμές είναι σε
// milliseconds.

DB.check = function() {
	var tora;

	console.log('DB.check (' + globals.ora() + ')');
	tora = globals.torams();
	globals.walk(DB.pool, function(i, conn) {
		// Απελευθερώνονται συνδέσεις που πιθανότατα δεν απελευθερώθηκαν από
		// τα προγράμματα που τις έχουν δεσμεύσει (zombies).

		if (conn.active && (tora - conn.realAction > DB.zombie)) {
			console.log('SQL connection freed: ' + conn.index);
			conn.free();
			return;
		}

		// Επανενεργοποιούνται συνδέσεις που φαίνονται ανενεργές για αρκετά
		// μεγάλο χρονικό διάστημα.

		if (tora - conn.action > DB.timeout) {
			console.log('SQL connection timeout: ' + conn.index);
			conn.action = tora;
			conn.connectionGet().query('SELECT 1');
		}
	});
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////@

// Ακολουθεί η κλάση "DBSindesi" που παριστά συνδέσεις με την database. Πρόκειται
// για ενισχυμένα αντικείμενα συνδέσεων, δηλαδή συνδέσεις που εμπλουτίζονται με
// επιπλέλον properties και μεθόδους.

DBSindesi = function() {
	var bekadb = '@p#a@$r*%09##o c$$#@!t@..:'.replace(/[^a-zA-Z0-9]/g, '');

	// Το property "active" δείχνει αν το connection είναι ενεργό, δηλαδή
	// αν το connection δεν έχει τοποθετηθεί ή επανατοποθετηθεί στο free
	// stack.

	this.active = true;

	// Το property "action" δείχνει τη χρονική στιγμή της τελευταίας επαφής
	// του connection με την database, ενώ το property "realAction" δείχνει
	// τη χρονική στιγμή που το connection χρησιμοποιήθηκε από το πρόγραμμα
	// για να φέρει εις πέρας κάποιο πραγματικό query. Όταν λέμε πραγματικό
	// query εννοούμε query ουσίας και όχι ψευδοquery που δίδεται για να
	// κρατήσει "ζωντανή" τη σύνδεση.

	this.realAction = (this.action = globals.torams());

	// Το property "connection" είναι η καρδιά του connection. Πρόκειται
	// για το connection αυτό καθεαυτό.

	this.connection = DB.mysql.createConnection({
		host: 'localhost',
		database: 'fantasmatakia',
		user: 'fantasmatakia',
		password: bekadb,
	});

	// Αμέσως μετά τη δημιουργία νέας σύνδεσης με την database, τοποθετούμε
	// τη νέα σύνδεση στο connection pool και κρατάμε τη θέση στo property
	// "index".

	this.index = DB.pool.push(this) - 1;
	console.log('new database connection: ' + this.index);
}

// Η μέθοδος "escape" χρησιμοποιείται κυρίως στην κατασκευή των queries και σκοπό
// έχει την προφύλαξη από SQL injections και το escaping των ειδικών χαρακτήρων.

DBSindesi.prototype.escape = function(s) {
	return this.connection.escape(s);
}

// Η μέθοδος "query" είναι αυτή που αναλαμβάνει να εκτελέσει τα queries του ανά χείρας
// connection. Ως παραμέτρους δέχεται το query αυτό καθεαυτό και (προαιρετικά) μια
// callback function που, εφόσον έχει δοθεί, θα κληθεί με παραμέτρους την ίδια τη
// σύνδεση και το αποτέλεσμα του query ως array από τα επιλεγέντα rows. Μετά την
// εκτέλεση του query, εμπλουτίζουμε την σύνδεση με δύο properties που δείχνουν
// το πλήθος των affected rows και το last inserted row id.

DBSindesi.prototype.query = function(query, callback) {
	var conn = this;

	this.connPrepare(query).connectionGet().query(query, function(err, res) {
		if (err) throw err;

		// Μετά την εκτέλεση του query κρατάμε ως properties της σύνδεσης
		// το πλήθος των affected rows και το last inserted row id, οπότε
		// μπορούμε να τα έχουμε πρόχειρα και εκτός αποτελεσμάτων.

		conn.affectedRows = res.affectedRows;
		conn.insertId = res.insertId;

		// Εφόσον έχει καθοριστεί callback function, την καλούμε με παραμέτρους
		// την ίδια τη σύνδεση και το array των αποτελεσμάτων.

		if (callback) callback(conn, res);
	});

	return this;
}

// Η μέθοδος "queryRaw" είναι παρόμοια με την "query", αλλά καλεί την callback function
// με τις παραμέτρους που καλείται και πραγματική μέθοδος "query" της database connection,
// δηλαδή με τα λάθη και τα αποτελέσματα. Ουσιαστικά πρόκειται για υποκατάστατο της
// πραγματικής "query" με κύρια διαφορά την ενημέρωση του active timestamp.

DBSindesi.prototype.queryRaw = function(query, callback) {
	this.connPrepare(query).connectionGet().query(query, callback);
	return this;
}

DBSindesi.prototype.connPrepare = function(query) {
	if (!this.active) globals.fatal(query + ': inactive database connection');
	Log.sql('connection: ' + this.index + '\n' + query);
	this.realAction = (this.action = globals.torams());
	return this;
}

DBSindesi.prototype.connectionGet = function() {
	return this.connection;
}

// Μετά το πέρας των εργασιών που επιτελούνται στα πλαίσια κάποιας database connection,
// πρέπει να καλείται η μέθοδος "free" η οποία επιστρέφει το connection στο free stack
// ώστε να μπορεί να ξαναχρησιμοποιηθεί.

DBSindesi.prototype.free = function() {
	if (!this.active) globals.fatal('inactive database connection pushed free');
	this.active = false;
	DB.freeStack.push(this.index);
	return this;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////@

// Η μέθοδος "transaction" εκκινεί μια σειρά από queries που θα πρέπει να εκτελεστούν ως
// ενιαίο transaction. Η μέθοδος δέχεται (υποχρεωτικά) μια callback function που θα είναι
// αυτή που θα παραλάβει τη σκυτάλη, πιθανόν για να εκτελέσει το πρώτο query. Η callback
// function καλείται με παράμετρο την ίδια τη σύνδεση.

DBSindesi.prototype.transaction = function(callback) {
	this.query('START TRANSACTION', function(conn) {
		callback(conn);
	});

	return this;
}

// Η μέθοδος "commit" κλείνει μια σειρά από queries που είχαν εκκινήσει με τη μέθοδο
// "transaction" κάνοντας commit. Ως παράμετρο μπορούμε να περάσουμε callback function
// που θα κληθεί με παράμετρο την ίδια τη σύνδεση. Αν δεν περάσουμ callback function,
// τότε η σύνδεση απελευθερώνεται και επιστρέφεται στο free stack.

DBSindesi.prototype.commit = function(callback) {
	this.query('COMMIT', function(conn) {
		if (callback) callback(conn);
		else conn.free();
	});

	return this;
}

// Η μέθοδος "rollback" κλείνει μια σειρά από queries που είχαν εκκινήσει με τη μέθοδο
// "transaction" κάνοντας rollback, ακυρώνοντας ουσιαστικά τις όποιες αλλαγές τα queries
// αυτά επέφεραν στην database. Ως παράμετρο μπορούμε να περάσουμε callback function
// που θα κληθεί με παράμετρο την ίδια τη σύνδεση. Αν δεν περάσουμ callback function,
// τότε η σύνδεση απελευθερώνεται και επιστρέφεται στο free stack.

DBSindesi.prototype.rollback = function(callback) {
	this.query('ROLLBACK', function(conn) {
		if (callback) callback(conn);
		else conn.free();
	});

	return this;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////@

// Η μέθοδος "klidoma" δέχεται κάποιο string που ονομάζεται tag και θέτει κλείδωμα για
// το συγκεκριμένο tag. Αν επιτύχει το κλείδωμα καλείται η callback function που περνάμε
// ως "onsuccess" property στην παράμετρο επιλογών με παρμέτρους την ίδια τη σύνδεση και
// το tag. Αν δεν έχει καθοριστεί property "onsuccess", η σύνδεση απελευθερώνεται. Αν
// αποτύχει το κλείδωμα, καλείται η callback function που περνάμε ως "onerror" property
// στην παράμετρο επιλογών με παραμέτρους τη σύνδεση και το tag string. Στην παράμετρο
// επιλογών μπορούμε επίσης να καθορίσουμε lock timeout σε seconds ως "timeout" property.

DBSindesi.prototype.klidoma = function(tag, opts) {
	var query;

	if (opts === undefined) opts = {};
	if (!opts.hasOwnProperty('onerror')) opts.onerror = DBSindesi.klidomeno;
	if (!opts.hasOwnProperty('timeout')) opts.timeout = 2;

	query = 'SELECT GET_LOCK(' + this.escape(tag) + ', ' + opts.timeout + ') AS `lock`';
	this.query(query, function(conn, rows) {
		if (rows.length != 1) return opts.onerror(conn, tag);
		if (rows[0]['lock'] != 1) return opts.onerror(conn, tag);
		if (!opts.hasOwnProperty('onsuccess')) conn.free();
		else opts.onsuccess(conn, tag);
	});

	return this;
}

// Η function "klidomeno" χρησιμοποιείται ως default error function σε περίπτωση
// που κάποιο κλείδωμα αποτύχει.

DBSindesi.klidomeno = function(conn, tag) {
	conn.free();
	throw tag + ': database lock exists'
}

// Η μέθοδος "xeklidoma" δέχεται ως παράμετρο ένα lock tag και αποσύρει το κλείδωμα
// για το συγκεκριμένο tag. Ως δεύτερη παράμετρο μπορούμε να περάσουμε callback
// function η οποία θα κληθεί με παράμετρο την ίδια τη σύνδεση και το lock tag.
// Αν δεν καθοριστεί callback function, η σύνδεση απελευθερώνεται.

DBSindesi.prototype.xeklidoma = function(tag, callback) {
	var query = 'DO RELEASE_LOCK(' + this.escape(tag) + ')';

	this.query(query, function(conn) {
		if (callback === undefined) conn.free();
		else callback(conn, tag);
	});

	return this;
}
